package com.wlyuan.index.domain.index.service;

import com.alibaba.fastjson2.JSON;
import com.wlyuan.core.domain.valueobj.CombineId;
import com.wlyuan.index.domain.index.CombineIndex;
import com.wlyuan.index.domain.index.repository.CombineIndexRepository;
import com.wlyuan.index.domain.index.repository.old.CombineIndexOld;
import com.wlyuan.index.domain.index.repository.old.CombineIndexOldRepository;
import com.wlyuan.index.domain.index.service.reindex.CombineReindexService;
import com.wlyuan.index.domain.index.valueobj.CombineRelation;
import com.wlyuan.index.domain.source.utils.SourceUtils;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import org.redisson.api.RBucket;
import org.redisson.api.RMapCache;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class CombineIndexDiffService {
    @Autowired
    private CombineIndexRepository indexRepository;
    @Autowired
    private CombineIndexOldRepository indexOldRepository;
    @Autowired
    private CombineReindexService reindexService;
    @Autowired
    private RedissonClient redissonClient;
    private RMapCache<CombineId, List<Diff>> diffs;
    private RSet<CombineId> ignores;
    private RBucket<Integer> page;

    @Mapper(uses = {SourceUtils.class})
    interface Converter {
        @Mapping(target = "deviceCode", source = "trackNo")
        @Mapping(target = "feeGenerated", source = "hasFees")
        @Mapping(target = "quotationStatus", source = "hasQuote")
        @Mapping(target = "no", source = "combineNo")
        CombineIndexOld convert(CombineIndex source);


        @Mapping(target = "quotationStatus", ignore = true)
        void convert(@MappingTarget CombineIndexOld index, CombineRelation relation);
    }

    private static final Converter CONVERTER = Mappers.getMapper(Converter.class);

    @Data
    @Getter
    @AllArgsConstructor
    public static class Diff implements Serializable {
        private CombineIndexOld old;
        private CombineIndexOld sub;
    }


    @PostConstruct
    public void initialize() {
        this.diffs = redissonClient.getMapCache("PC::CACHE::INDEX::COMBINE::DIFFS");
        this.ignores = redissonClient.getSet("PC::CACHE::INDEX::COMBINE::DIFFS::IGNORES");
        this.page = redissonClient.getBucket("PC::CACHE::INDEX::COMBINE::DIFFS::PAGE");
    }

    private BigDecimal formatValue(BigDecimal value) {
        if (null == value) {
            return null;
        }
        return value.setScale(2, RoundingMode.UP);
    }

    private void add(CombineId combineId, CombineIndexOld oldIndex, CombineIndexOld subIndex) {
        var diff = this.diffs.get(combineId);
        if (null == diff) {
            diff = new ArrayList<>();
        }
        diff.add(new Diff(oldIndex, subIndex));
        this.diffs.fastPut(combineId, diff, 1, TimeUnit.DAYS);
    }

    public void diff(CombineId combineId) {
        if (ignores.contains(combineId)) {
            logger.warn("Combine index diff ignored: {}", combineId);
            return;
        }

        reindexService.reindex(combineId);
        var oldIndexes = indexOldRepository.list(combineId);
        var newIndex = indexRepository.ofId(combineId);
        if (oldIndexes.size() != newIndex.getRelations().size()) {
            logger.error("Combine relations size no match: {} {}", oldIndexes, newIndex);
            return;
        }

        var relationMap = newIndex.getRelations()
                .stream()
                .collect(Collectors.toMap(CombineRelation::getTenantId, i -> i));

        oldIndexes.forEach(oldIndex -> {
            var subIndex = CONVERTER.convert(newIndex);
            var relation = relationMap.get(oldIndex.getTenantId());
            if (null != relation) {
                CONVERTER.convert(subIndex, relation);
            }

            oldIndex.setCalculateVolume(formatValue(oldIndex.getCalculateVolume()));
            oldIndex.setCalculateWeight(formatValue(oldIndex.getCalculateWeight()));

            subIndex.setCalculateVolume(formatValue(subIndex.getCalculateVolume()));
            subIndex.setCalculateWeight(formatValue(subIndex.getCalculateWeight()));
            if (!oldIndex.equals(subIndex)) {
                logger.error("Combine index no match: {} {}", oldIndex, subIndex);
                logger.info("Combine index old: {}", JSON.toJSON(oldIndex));
                logger.info("Combine index sub: {}", JSON.toJSON(subIndex));
                this.add(combineId, oldIndex, subIndex);
            }
        });
    }


    private void diff(CombineIndexOld index) {
        var combineId = new CombineId(index.getCombineId());
        try {
            this.diff(combineId);
        } catch (Exception e) {
            logger.error("Combine index diff error: {}", index, e);
            this.ignores.add(combineId);
        }
    }

    public void diffAll() {
        // 对比新旧索引是否相同
        var page = null != this.page.get() ? this.page.get() : 1;
        var size = 10;
        var completedSize = 0;
        logger.info("Combine indexes diff start: {}", page);
        var oldIndexes = indexOldRepository.list(page++, size);
        while (!oldIndexes.getContent().isEmpty()) {
            oldIndexes.forEach(this::diff);
            completedSize = page * size + oldIndexes.getContent().size();
            logger.info("Combine index diff size: {}", completedSize);
            this.page.set(page);
            oldIndexes = indexOldRepository.list(page++, size);
        }
    }

    public Map<Long, List<Diff>> diffs() {
        var diffs = new HashMap<Long, List<Diff>>();
        this.diffs.forEach(((key, value) -> diffs.put(key.getId(), value)));
        return diffs;
    }

    public Set<Long> ignores() {
        return this.ignores.stream()
                .map(CombineId::getId)
                .collect(Collectors.toSet());
    }
}
